package xyz.icxl.android.architecture.webview;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.ResultReceiver;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.webkit.ConsoleMessage;
import android.webkit.CookieManager;
import android.webkit.GeolocationPermissions;
import android.webkit.JavascriptInterface;
import android.webkit.JsResult;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebStorage;
import android.webkit.WebView;

import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import static android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW;

import org.json.JSONArray;
import org.json.JSONObject;

import me.jessyan.autosize.AutoSize;

/**
 * # 这里写文件描述
 * - author：`cxl`
 * - date: `2022/11/17`
 */
public class MyWebView extends WebView {
    //windwos._dsbridge.call()
    //window._dsbridge.call()
    //windos.webkit.post("_dsbridge.call()")
    private static final String BRIDGE_NAME = "_dsbridge";
    //private Object jsb;
    private ArrayList<Object> jsbs = new ArrayList<>();
    private String APP_CACAHE_DIRNAME;
    int callID = 0;
    private static final int EXEC_SCRIPT = 1;
    private static final int LOAD_URL = 2;
    private static final int LOAD_URL_WITH_HEADERS = 3;
    MyHandler mainThreadHandler = null;

    class MyHandler extends Handler {
        //  Using WeakReference to avoid memory leak
        WeakReference<Activity> mActivityReference;

        MyHandler(Activity activity) {
            mActivityReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            final Activity activity = mActivityReference.get();
            if (activity != null) {
                switch (msg.what) {
                    case EXEC_SCRIPT:
                        _evaluateJavascript((String) msg.obj);
                        break;
                    case LOAD_URL:
                        MyWebView.super.loadUrl((String) msg.obj);
                        break;
                    case LOAD_URL_WITH_HEADERS: {
                        RequestInfo info = (RequestInfo) msg.obj;
                        MyWebView.super.loadUrl(info.url, info.headers);
                    }
                    break;
                }
            }
        }
    }

    @Deprecated
    public interface FileChooser {
        @TargetApi(Build.VERSION_CODES.HONEYCOMB)
        void openFileChooser(ValueCallback valueCallback, String acceptType);

        @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
        void openFileChooser(ValueCallback<Uri> valueCallback,
                             String acceptType, String capture);
    }

    class RequestInfo {
        String url;
        Map<String, String> headers;

        RequestInfo(String url, Map<String, String> additionalHttpHeaders) {
            this.url = url;
            this.headers = additionalHttpHeaders;
        }
    }


    Map<Integer, OnReturnValue> handlerMap = new HashMap<>();

    public MyWebView(@NonNull Context context) {
        super(context);
        init();
    }

    public MyWebView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MyWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public void setOverScrollMode(int mode) {
        super.setOverScrollMode(mode);
        AutoSize.autoConvertDensityOfGlobal((Activity) getContext());
    }


    @Keep
    void init() {
        mainThreadHandler = new MyHandler((Activity) getContext());
        APP_CACAHE_DIRNAME = getContext().getFilesDir().getAbsolutePath() + "/webcache";
        WebSettings settings = getSettings();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            CookieManager.getInstance().setAcceptThirdPartyCookies(this, true);
        }
        settings.setAllowFileAccess(false);
        settings.setSavePassword(false);
        settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
        settings.setJavaScriptEnabled(true);
        settings.setLoadWithOverviewMode(true);
        settings.setSupportMultipleWindows(true);
        settings.setDomStorageEnabled(true);
        settings.setDatabaseEnabled(true);
        settings.setGeolocationDatabasePath(getContext().getDir("database",Context.MODE_PRIVATE).getPath());
        settings.setGeolocationEnabled(true);

        if (Build.VERSION.SDK_INT >= 21) {
            settings.setMixedContentMode(MIXED_CONTENT_ALWAYS_ALLOW);
        }
        settings.setUseWideViewPort(true);
        super.setWebChromeClient(mWebChromeClient);
        super.addJavascriptInterface(new Object() {
            private HashMap<String,Long> callTime = new HashMap<>();
            @Keep
            @JavascriptInterface
            public String call(String methodName, String args) {
                JSONObject ret = new JSONObject();
                Log.e("MyWeb", "前端调用：methodName：" + methodName + "\t args:" + args);
                String error = "Js bridge method called, but there is " +
                        "not a JavascriptInterface object, please set JavascriptInterface object first!";
                if(methodName.equals("_dsb.dsinit")){
                    return  "";
                }
                for (Object jsb : jsbs) {
                    if (jsb == null) {
                        Log.e("SynWebView", error);
                        return "";
                    }

                    Class<?> cls = jsb.getClass();
                    try {
                        Method method = null;
                        boolean asyn = false;
                        JSONObject arg = new JSONObject(args);

                        String callback = "";
                        if (!arg.isNull("_dscbstub")) {
                            callback = arg.getString("_dscbstub");
                        }

                        if (!arg.isNull("data")) {
                            arg = arg.getJSONObject("data");
                        }
                        try {
                            //   callback = arg.getString("_dscbstub");
                            arg.remove("_dscbstub");
                            arg.remove("_dsb.dsinit");
                            method = cls.getDeclaredMethod(methodName,
                                    new Class[]{JSONObject.class, CompletionHandler.class});
                            asyn = true;
                        } catch (Exception e) {
                            try {
                                method = cls.getDeclaredMethod(methodName, new Class[]{JSONObject.class});
                            }catch (Exception e2){
                                e2.printStackTrace();
                            }
                        }

                        if (method == null) {
                            error = "ERROR! \n Not find method \"" + methodName + "\" implementation! ";
                            Log.e("SynWebView", error);
                            String finalError = error;
                            mainThreadHandler.post(() -> {
                                mWebChromeClient.onJsAlert(MyWebView.this,getUrl(), finalError,null);
                            });
                            return "ERROR";
                        }
                        JavascriptInterface annotation = method.getAnnotation(JavascriptInterface.class);
                        if (annotation != null) {
                            Object retData;
                            method.setAccessible(true);
                            if (asyn) {
                                final String cb = callback;
                                retData = method.invoke(jsb, arg, new CompletionHandler() {

                                    @Override
                                    public void complete(String retValue) {
                                        Log.e("MyWeb","前端调用("+methodName+")异步返回->"+retValue);
                                        complete(retValue, true);
                                    }

                                    @Override
                                    public void complete() {
                                        Log.e("MyWeb","前端调用("+methodName+")异步返回->无参");
                                        complete("", true);
                                    }

                                    @Override
                                    public void setProgressData(String value) {
                                        complete(value, false);
                                    }

                                    private void complete(String retValue, boolean complete) {
                                        try {
                                            if (retValue == null) retValue = "";
                                            retValue = URLEncoder.encode(retValue, "UTF-8").replaceAll("\\+", "%20");
                                            String script = String.format("%s(decodeURIComponent(\"%s\"));", cb, retValue);
                                            if (complete) {
                                                script += "delete window." + cb;
                                            }
                                            evaluateJavascript(script);
                                        } catch (UnsupportedEncodingException e) {
                                            e.printStackTrace();
                                        }
                                    }
                                });
                            } else {
                                retData = method.invoke(jsb, arg);
                            }
                            if (retData == null) {
                                retData = "";
                            }
                            Log.e("MWeb", "前端调用：" + methodName + "；函数返回：" + retData.toString());
                            return retData.toString();
                        } else {
                            error = "Method " + methodName + " is not invoked, since  " +
                                    "it is not declared with JavascriptInterface annotation! ";
                            Log.e("SynWebView", error);
                        }
                    } catch (Exception e) {
                        //evaluateJavascript(String.format("alert('ERROR! \\nCall failed：Function does not exist or parameter is invalid［%s］')", e.getMessage()));
                        e.printStackTrace();
                    }
                }

                return "";
            }

            @Keep
            @JavascriptInterface
            public void returnValue(int id, String value) {
                OnReturnValue handler = handlerMap.get(id);
                if (handler != null) {
                    handler.onValue(value);
                    handlerMap.remove(id);
                }
            }
        }, BRIDGE_NAME);

    }

    @Override
    public void setWebChromeClient(WebChromeClient client) {
        webChromeClient = client;
    }

    WebChromeClient webChromeClient;

    private final WebChromeClient mWebChromeClient = new WebChromeClient() {

        @Override
        public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
            if (webChromeClient != null)
                return webChromeClient.onJsAlert(view, url, message, result);
            else
                return super.onJsAlert(view, url, message, result);
        }

        @Override
        public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
            if (webChromeClient != null)
                return webChromeClient.onJsConfirm(view, url, message, result);
            else
                return super.onJsConfirm(view, url, message, result);
        }

        @Override
        public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
            if (webChromeClient != null)
                return webChromeClient.onConsoleMessage(consoleMessage);
            else
                return super.onConsoleMessage(consoleMessage);
        }

        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            injectJs();
            if (webChromeClient != null) {
                webChromeClient.onProgressChanged(view, newProgress);
            } else {
                super.onProgressChanged(view, newProgress);
            }
        }

        @Override
        public void onReceivedTitle(WebView view, String title) {
            injectJs();
            if (webChromeClient != null) {
                webChromeClient.onReceivedTitle(view, title);
            } else {
                super.onReceivedTitle(view, title);
            }
        }

        @Override
        public void onReceivedIcon(WebView view, Bitmap icon) {
            if (webChromeClient != null) {
                webChromeClient.onReceivedIcon(view, icon);
            } else {
                super.onReceivedIcon(view, icon);
            }
        }

        @Override
        public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) {
            if (webChromeClient != null) {
                webChromeClient.onReceivedTouchIconUrl(view, url, precomposed);
            } else {
                super.onReceivedTouchIconUrl(view, url, precomposed);
            }
        }

        @Override
        public void onShowCustomView(View view, CustomViewCallback callback) {
            if (webChromeClient != null) {
                webChromeClient.onShowCustomView(view, callback);
            } else {
                super.onShowCustomView(view, callback);
            }
        }


        @Override
        public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) {
            if (webChromeClient != null) {
                webChromeClient.onShowCustomView(view, requestedOrientation, callback);
            } else {
                super.onShowCustomView(view, requestedOrientation, callback);
            }
        }

        @Override
        public void onHideCustomView() {
            if (webChromeClient != null) {
                webChromeClient.onHideCustomView();
            } else {
                super.onHideCustomView();
            }
        }

        @Override
        public boolean onCreateWindow(WebView view, boolean isDialog,
                                      boolean isUserGesture, Message resultMsg) {
            if (webChromeClient != null) {
                return webChromeClient.onCreateWindow(view, isDialog,
                        isUserGesture, resultMsg);
            }
            return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg);
        }

        @Override
        public void onRequestFocus(WebView view) {
            if (webChromeClient != null) {
                webChromeClient.onRequestFocus(view);
            } else {
                super.onRequestFocus(view);
            }
        }

        @Override
        public void onCloseWindow(WebView window) {
            if (webChromeClient != null) {
                webChromeClient.onCloseWindow(window);
            } else {
                super.onCloseWindow(window);
            }
        }


        @Override
        public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
            if (webChromeClient != null) {
                return webChromeClient.onJsBeforeUnload(view, url, message, result);
            }
            return super.onJsBeforeUnload(view, url, message, result);
        }

        @Override
        public void onExceededDatabaseQuota(String url, String databaseIdentifier, long quota,
                                            long estimatedDatabaseSize,
                                            long totalQuota,
                                            WebStorage.QuotaUpdater quotaUpdater) {
            if (webChromeClient != null) {
                webChromeClient.onExceededDatabaseQuota(url, databaseIdentifier, quota,
                        estimatedDatabaseSize, totalQuota, quotaUpdater);
            } else {
                super.onExceededDatabaseQuota(url, databaseIdentifier, quota,
                        estimatedDatabaseSize, totalQuota, quotaUpdater);
            }
        }

        @Override
        public void onGeolocationPermissionsHidePrompt() {
            if (webChromeClient != null) {
                webChromeClient.onGeolocationPermissionsHidePrompt();
            } else {
                super.onGeolocationPermissionsHidePrompt();
            }
        }

        @Override
        public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
            if (webChromeClient != null) {
                webChromeClient.onGeolocationPermissionsShowPrompt(origin, callback);
            } else {
                super.onGeolocationPermissionsShowPrompt(origin, callback);
            }
        }


        @Override
        public boolean onJsTimeout() {
            if (webChromeClient != null) {
                return webChromeClient.onJsTimeout();
            }
            return super.onJsTimeout();
        }


        @Override
        public Bitmap getDefaultVideoPoster() {

            if (webChromeClient != null) {
                return webChromeClient.getDefaultVideoPoster();
            }
            return super.getDefaultVideoPoster();
        }

        @Override
        public View getVideoLoadingProgressView() {
            if (webChromeClient != null) {
                return webChromeClient.getVideoLoadingProgressView();
            }
            return super.getVideoLoadingProgressView();
        }

        @Override
        public void getVisitedHistory(ValueCallback<String[]> callback) {
            if (webChromeClient != null) {
                webChromeClient.getVisitedHistory(callback);
            } else {
                super.getVisitedHistory(callback);
            }
        }

        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        @Override
        public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
                                         FileChooserParams fileChooserParams) {
            if (webChromeClient != null) {
                return webChromeClient.onShowFileChooser(webView, filePathCallback, fileChooserParams);
            }
            return super.onShowFileChooser(webView, filePathCallback, fileChooserParams);
        }

        @Keep
        @TargetApi(Build.VERSION_CODES.HONEYCOMB)
        public void openFileChooser(ValueCallback valueCallback, String acceptType) {
            if (webChromeClient instanceof FileChooser) {
                ((FileChooser) webChromeClient).openFileChooser(valueCallback, acceptType);
            }
        }
    };

    private void injectJs() {
        evaluateJavascript("function getJsBridge(){window._dsf=window._dsf||{};return{call:function(b,a,c){\"function\"==typeof a&&(c=a,a={});if(\"function\"==typeof c){window.dscb=window.dscb||0;var d=\"dscb\"+window.dscb++;window[d]=c;a._dscbstub=d}a=JSON.stringify(a||{});return window._dswk?prompt(window._dswk+b,a):\"function\"==typeof _dsbridge?_dsbridge(b,a):_dsbridge.call(b,a)},register:function(b,a){\"object\"==typeof b?Object.assign(window._dsf,b):window._dsf[b]=a}}}dsBridge=getJsBridge();");
    }

    private void _evaluateJavascript(String script) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            MyWebView.super.evaluateJavascript(script, null);
        } else {
            loadUrl("javascript:" + script);
        }
    }

    public void evaluateJavascript(final String script) {
        if (Looper.getMainLooper() == Looper.myLooper()) {
            _evaluateJavascript(script);
        } else {
            Message msg = new Message();
            msg.what = EXEC_SCRIPT;
            msg.obj = script;
            mainThreadHandler.sendMessage(msg);
        }
    }

    @Override
    public void clearCache(boolean includeDiskFiles) {
        super.clearCache(includeDiskFiles);
        CookieManager.getInstance().removeAllCookie();
        Context context = getContext();
        try {
            context.deleteDatabase("webview.db");
            context.deleteDatabase("webviewCache.db");
        } catch (Exception e) {
            e.printStackTrace();
        }

        File appCacheDir = new File(APP_CACAHE_DIRNAME);
        File webviewCacheDir = new File(context.getCacheDir()
                .getAbsolutePath() + "/webviewCache");

        if (webviewCacheDir.exists()) {
            deleteFile(webviewCacheDir);
        }

        if (appCacheDir.exists()) {
            deleteFile(appCacheDir);
        }
    }

    public void deleteFile(File file) {
        if (file.exists()) {
            if (file.isFile()) {
                file.delete();
            } else if (file.isDirectory()) {
                File files[] = file.listFiles();
                for (int i = 0; i < files.length; i++) {
                    deleteFile(files[i]);
                }
            }
            file.delete();
        } else {
            Log.e("Webview", "delete file no exists " + file.getAbsolutePath());
        }
    }

    @Override
    public void loadUrl(String url) {
        Message msg = new Message();
        msg.what = LOAD_URL;
        msg.obj = url;
        mainThreadHandler.sendMessage(msg);
    }

    public void callHandler(String method, Object[] args) {
        callHandler(method, args, null);
    }

    @SuppressLint("DefaultLocale")
    public void callHandler(String method, Object[] args, final OnReturnValue handler) {
        if (args == null) args = new Object[0];
        String arg = new JSONArray(Arrays.asList(args)).toString();
        String script = String.format("(window._dsf.%s||window.%s).apply(window._dsf||window,%s)", method, method, arg);
        if (handler != null) {
            script = String.format("%s.returnValue(%d,%s)", BRIDGE_NAME, callID, script);
            handlerMap.put(callID++, handler);
        }
        evaluateJavascript(script);

    }


    @Override
    public void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
        Message msg = new Message();
        msg.what = LOAD_URL_WITH_HEADERS;
        msg.obj = new RequestInfo(url, additionalHttpHeaders);
        mainThreadHandler.sendMessage(msg);
    }

    public void addJavascriptInterface(Object object) {
        jsbs.add(object);
    }

    public interface OnReturnValue {
        void onValue(String retValue);
    }

    public interface CompletionHandler {
        void complete(String retValue);

        void complete();

        void setProgressData(String value);
    }

}
